-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/stripe #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/stripe #66
Conversation
jorgebaralt
commented
Sep 14, 2025
- Add better Auth Stripe plugin
- Manage Stripe subscription from the new subscription modal
…hook, local listener)
rmolinamir
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR broke the build CI :
Error: Neither apiKey nor config.authenticator provided
See: stripe/stripe-js#667
| const handleClick = () => { | ||
| setCancelUrl(pathname); | ||
|
|
||
| // Add a small delay to allow the dropdown to close before navigation | ||
| setTimeout(() => { | ||
| router.push("/auth/settings"); | ||
| }, 150); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| quota: 250, // 2.5 USD | ||
| }, | ||
| name: "pro", | ||
| priceId: "price_1S2gQfK8Pm3Qm3VvhM5TuvWI", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs to be configurable/parametized.
| @@ -0,0 +1,11 @@ | |||
| { | |||
| "name": "@acme/stripe-listener", | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Rename to stripe-tunnel or just stripe in case we add more local development goodies.
| "type": "module", | ||
| "scripts": { | ||
| "clean": "git clean -xdf .cache .turbo node_modules tsconfig.tsbuildinfo", | ||
| "dev": "stripe listen --forward-to localhost:3000/api/auth/stripe/webhook", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be configurable (see localtunnel).
| const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => { | ||
| setIsLoading(true); | ||
|
|
||
| if (!sessionData?.user?.id) { | ||
| toast.error("Please sign in to manage your subscription"); | ||
| return; | ||
| } | ||
|
|
||
| const result = await authClient.subscription.billingPortal({ | ||
| locale: "en", | ||
| referenceId: sessionData.user.id, | ||
| returnUrl: pathname, | ||
| }); | ||
|
|
||
| if (result.error) { | ||
| console.error("Billing portal error:", result.error); | ||
| toast.error("Failed to open billing portal. Please try again."); | ||
| } | ||
|
|
||
| // Call the custom onClick handler if provided | ||
| onClick?.(event); | ||
| setIsLoading(false); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use react-query instead?
| {isOnSubscriptionPage ? ( | ||
| <div className="flex items-center gap-2"> | ||
| <CreditCard /> | ||
| Subscription | ||
| </div> | ||
| ) : ( | ||
| <Link href="/subscription" className="flex items-center gap-2"> | ||
| <CreditCard /> | ||
| Subscription | ||
| </Link> | ||
| )} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Do we need to do this?
| <DropdownMenuItem asChild className="w-full cursor-pointer"> | ||
| <form action={signOutAction} className="w-full"> | ||
| <button type="submit" className="flex w-full items-center gap-2"> | ||
| <LogOut /> | ||
| Sign out | ||
| </button> | ||
| </form> | ||
| </DropdownMenuItem> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happened here?
| // Use effect to prevent hydration mismatch and flashing | ||
| useEffect(() => { | ||
| setIsOpen(pathname === "/subscription"); | ||
| }, [pathname]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: I'll check this later
| error?: Error; | ||
| } | ||
|
|
||
| export class BillingErrorBoundary extends Component<Props, State> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
class?
| collapsible="offcanvas" | ||
| variant="floating" | ||
| className="sticky top-0 h-svh" | ||
| className="sticky top-0 z-10 h-svh" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably unnecessary 🤔
| "module": "ES2022", | ||
| "moduleResolution": "bundler", | ||
| "skipLibCheck": true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this needed? Not ESM friendly I think (which is not ideal).
| id: text("id").primaryKey(), | ||
| plan: text("plan").notNull(), | ||
| referenceId: text("reference_id").notNull(), | ||
| stripeCustomerId: text("stripe_customer_id"), | ||
| stripeSubscriptionId: text("stripe_subscription_id"), | ||
| status: text("status").default("incomplete"), | ||
| periodStart: timestamp("period_start"), | ||
| periodEnd: timestamp("period_end"), | ||
| trialStart: timestamp("trial_start"), | ||
| trialEnd: timestamp("trial_end"), | ||
| cancelAtPeriodEnd: boolean("cancel_at_period_end"), | ||
| seats: integer("seats"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I like keeping the underscores to avoid accidental bugs (skill issue lol).
| } from "drizzle-orm/pg-core"; | ||
|
|
||
| // Create the subscription table | ||
| export const subscription = pgTable("subscription", { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing user_id foreign key.
| updatedAt: timestamp("updated_at") | ||
| .$defaultFn(() => /* @__PURE__ */ new Date()) | ||
| .notNull(), | ||
| stripeCustomerId: text("stripe_customer_id"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this, rely on foreign key on the subscription table instead.
| id: text("id").primaryKey(), | ||
| plan: text("plan").notNull(), | ||
| referenceId: text("reference_id").notNull(), | ||
| stripeCustomerId: text("stripe_customer_id"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably needs to be unique.